package com.heima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.config.MinioProp;
import com.heima.feign.ArticleFeignClient;
import com.heima.feign.SearchFeignClient;
import com.heima.feign.UserFeignClient;
import com.heima.mapper.ArticleMapper;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.dtos.ArticleInfoDto;
import com.heima.model.article.dtos.CollectionBehaviorDto;
import com.heima.model.article.mess.ArticleVisitStreamMess;
import com.heima.model.article.pojos.Article;
import com.heima.model.article.pojos.ArticleConfig;
import com.heima.model.article.pojos.ArticleContent;
import com.heima.model.article.pojos.CollectionBehavior;
import com.heima.model.article.vo.HotArticleVo;
import com.heima.model.behavior.pojos.LikesBehavior;
import com.heima.model.behavior.pojos.UnLikesBehavior;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.constants.ArticleConstants;
import com.heima.model.constants.BehaviorConstants;
import com.heima.model.search.vos.SearchArticleVo;
import com.heima.model.user.pojos.UserRelation;
import com.heima.service.ArticleContentService;
import com.heima.service.ArticleService;
import com.heima.utils.MinioUtils;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.minio.errors.*;
import lombok.val;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author chenshan
 * @description 针对表【ap_article(文章信息表，存储已发布的文章)】的数据库操作Service实现
 * @createDate 2022-10-14 17:01:13
 */
@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
        implements ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private ArticleConfigServiceImpl configService;

    @Autowired
    private ArticleContentService contentService;

    @Autowired
    private MinioUtils minioUtils;

    @Autowired
    private MinioProp minioProp;

    @Autowired
    private Configuration configuration;

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private ArticleFeignClient articleFeignClient;

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private SearchFeignClient searchFeignClient;

    @Autowired
    private RestHighLevelClient client;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * APP端文章首页数据请求
     *
     * @param homeDto homeDto
     * @return ResponseResult
     */
    @Override
    public ResponseResult load(ArticleHomeDto homeDto) {
        // 校验参数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) {
            homeDto.setSize(10);
        }

        // 获取要查询的频道id

        // 如果频道id为空，我们默认查询 __all__
        String tag = homeDto.getTag();
        if (StrUtil.isEmpty(tag)) {
            tag = "__all__";
        }
        String s = redisTemplate.opsForValue().get(ArticleConstants.ARTICLE_CHANNEL + tag);
        List<HotArticleVo> hotArticleVos = JSONUtil.toList(s, HotArticleVo.class);
        if (hotArticleVos.size() > homeDto.getSize()) {
            hotArticleVos.subList(0, homeDto.getSize());
        }
        return ResponseResult.okResult(hotArticleVos);

        // return ResponseResult.okResult(articleMapper.load(homeDto));
    }

    /**
     * 下拉加载最新数据
     *
     * @param homeDto homeDto
     * @return ResponseResult
     */
    @Override
    public ResponseResult loadNew(ArticleHomeDto homeDto) {
        // 校验参数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) {
            homeDto.setSize(10);
        }
        if (ObjectUtil.isNull(homeDto.getTag())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        return ResponseResult.okResult(articleMapper.loadNew(homeDto));
    }

    /**
     * 上拉加载更多数据
     *
     * @param homeDto homeDto
     * @return ResponseResult
     */
    @Override
    public ResponseResult loadMore(ArticleHomeDto homeDto) {
        // 校验参数
        if (ObjectUtil.isNull(homeDto.getSize()) || homeDto.getSize() <= 0) {
            homeDto.setSize(10);
        }
        if (ObjectUtil.isNull(homeDto.getTag())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        return ResponseResult.okResult(articleMapper.loadMore(homeDto));
    }

    /**
     * 发布文章
     *
     * @param articleDto 文章内容
     * @return 文章id
     */
    @Override
    public ResponseResult saveArticle(ArticleDto articleDto) throws IOException, TemplateException, InvalidResponseException, InvalidKeyException, NoSuchAlgorithmException, ServerException, ErrorResponseException, XmlParserException, InvalidBucketNameException, InsufficientDataException, InternalException {
        // 参数校验 标题 内容 作者 频道
        if (!ObjectUtil.isAllNotEmpty(articleDto.getTitle(), articleDto.getContent(), articleDto.getAuthorName(), articleDto.getChannelName())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 保存数据到article表中
        Article article = new Article();
        BeanUtil.copyProperties(articleDto, article);

        // 保存article表数据
        save(article);

        // 保存数据到article_config表中
        ArticleConfig config = new ArticleConfig();
        config.setArticleId(article.getId());
        // 是否允许评论 - 默认允许
        config.setIsComment(1);
        // 是否已经删除 - 默认未删除
        config.setIsDelete(0);
        // 是否允许转发 - 默认允许
        config.setIsForward(1);
        // 是否已经下架 - 默认未下架
        config.setIsDown(0);

        configService.save(config);

        // 保存数据到article_content表中
        ArticleContent content = new ArticleContent();
        content.setArticleId(article.getId());
        content.setContent(articleDto.getContent());

        contentService.save(content);

        // 生成当前页面的静态资源页面
        //加载模板文件
        Template template = configuration.getTemplate("article.ftl");
        // 把文件直接上传到Minio中
        //输出 freemarker的数据模型的类型只能是map或是JavaBean对象
        HashMap<Object, Object> map = new HashMap<>();
        map.put("content", JSONUtil.parseArray(content.getContent()));
        StringWriter sw = new StringWriter();
        template.process(map, sw);

        // 上传静态页面到minio
        String fileName = DateTime.now().toString("yyyy/MM/dd") + "/" + article.getId() + ".html";
        minioUtils.putObject(minioProp.getBucket(), fileName, new ByteArrayInputStream(sw.toString().getBytes()), "text/html");
        // 手动拼接一个静态资源的访问地址
        String staticUrl = minioUtils.getBasisUrl() + fileName;
        // 把静态地址更新到文章表中
        article.setStaticUrl(staticUrl);
        updateById(article);

        // 保存数据到ES中
        SearchArticleVo searchArticleVo = new SearchArticleVo();
        BeanUtil.copyProperties(article, searchArticleVo);
        searchArticleVo.setContent(content.getContent());

        // searchFeignClient.saveArticle(searchArticleVo);
        IndexRequest request = new IndexRequest("article_index");
        request.id(String.valueOf(searchArticleVo.getId()));
        request.source(JSONUtil.toJsonStr(searchArticleVo), XContentType.JSON);

        client.index(request, RequestOptions.DEFAULT);

        return ResponseResult.okResult(article.getId());
    }

    @Override
    public ResponseResult findOneById(Long articleId) {
        // 校验参数的合法性
        if (ObjectUtil.isNull(articleId)) {
            ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 根据文章id查询文章详情
        Article article = getById(articleId);
        if (ObjectUtil.isNull(article)) {
            ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "文章不存在~");
        }

        // 创建文章Dto接收文章内容
        ArticleDto articleDto = new ArticleDto();
        BeanUtil.copyProperties(article, articleDto);

        // 查询文章内容
        ArticleContent content = contentService.getOne(Wrappers.lambdaQuery(ArticleContent.class).eq(ArticleContent::getArticleId, article.getId()));
        articleDto.setContent(content.getContent());

        return ResponseResult.okResult(articleDto);
    }

    @Override
    public ResponseResult findAll() {
        List<Article> articleList = articleMapper.selectList(null);

        ArrayList<ArticleDto> articleDtos = new ArrayList<>();
        for (Article article : articleList) {
            ArticleDto articleDto = new ArticleDto();
            BeanUtil.copyProperties(article, articleDto);

            ArticleContent content = contentService.getOne(Wrappers.lambdaQuery(ArticleContent.class).eq(ArticleContent::getArticleId, articleDto.getId()));
            if (ObjectUtil.isNotEmpty(content)) {
                articleDto.setContent(content.getContent());
            }

            articleDtos.add(articleDto);
        }
        return ResponseResult.okResult(articleDtos);
    }

    /**
     * 用户行为 - 文章收藏
     *
     * @param behaviorDto 收藏信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult collectionBehavior(CollectionBehaviorDto behaviorDto) {
        // 校验参数的合法性
        if (!ObjectUtil.isAllNotEmpty(behaviorDto.getEntryId(), behaviorDto.getOperation(), behaviorDto.getType(), behaviorDto.getPublishedTime())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 校验文章id对应的文章是否存在
        Article article = getById(behaviorDto.getEntryId());
        if (ObjectUtil.isEmpty(article)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "文章已不存在~");
        }

        // 接收用户id 和文章id
        // 查询用户id
        Integer userId = userFeignClient.selectUserId();
        Long articleId = behaviorDto.getEntryId();
        if (ObjectUtil.isEmpty(userId)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
        }

        // 用户收藏了文章
        if (behaviorDto.getOperation() == 0) {
            // 1.先判断Redis中是否有收藏记录
            Boolean userHasKey = redisTemplate.opsForHash().hasKey(BehaviorConstants.BEHAVIOR + userId,
                    BehaviorConstants.COLLECTION + articleId);
            Boolean articleHasKey = redisTemplate.opsForHash().hasKey(BehaviorConstants.BEHAVIOR + articleId,
                    BehaviorConstants.COLLECTION + userId);

            // 2.判断MongoDB中是否有收藏记录
            CollectionBehavior collectionBehavior = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(userId).and("entryId").is(articleId)), CollectionBehavior.class);

            // 3.Redis和MongoDB中存在收藏记录，不做任何操作
            if (ObjectUtil.isNotEmpty(collectionBehavior) && userHasKey && articleHasKey) {
                return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
            }

            // 4.Redis和MongoDB中不存在收藏记录或点赞记录中user与article不对应，       添加 == 值任意
            redisTemplate.opsForHash().put(BehaviorConstants.BEHAVIOR + userId, BehaviorConstants.COLLECTION + articleId, 1);
            redisTemplate.opsForHash().put(BehaviorConstants.BEHAVIOR + articleId, BehaviorConstants.COLLECTION + userId, 1);
            if (ObjectUtil.isNull(collectionBehavior)) {
                mongoTemplate.insert(CollectionBehavior.builder().userId(userId).entryId(articleId).operation(0).type(behaviorDto.getType()).publishedTime(behaviorDto.getPublishedTime()).build());
            }

            // 更新文章收藏数量
            if (ObjectUtil.isEmpty(article.getCollection())) {
                article.setCollection(0);
            }
            article.setCollection(article.getCollection() + 1);

        } else {
            // 用户取消收藏啦
            redisTemplate.opsForHash().delete(BehaviorConstants.BEHAVIOR + userId,
                    BehaviorConstants.COLLECTION + articleId);
            redisTemplate.opsForHash().delete(BehaviorConstants.BEHAVIOR + articleId,
                    BehaviorConstants.COLLECTION + userId);
            mongoTemplate.remove(Query.query(Criteria.where("userId").is(userId).and("entryId").is(articleId)), CollectionBehavior.class);

            // 更新文章收藏数量
            if (ObjectUtil.isNotEmpty(article.getCollection()) && article.getCollection() > 0) {
                article.setCollection(article.getCollection() - 1);
            }
        }

        // 修改收藏数量
        updateById(article);

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 加载文章行为 - 数据回显
     * @param infoDto 文章信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult loadArticleBehavior(ArticleInfoDto infoDto) {
        // 校验参数的合法性
        if (!ObjectUtil.isAllNotEmpty(infoDto.getArticleId(), infoDto.getAuthorId())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 校验文章id对应的文章是否存在
        ResponseResult result = articleFeignClient.findOneById(infoDto.getArticleId());
        if (ObjectUtil.isEmpty(result.getData())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "文章已不存在~");
        }

        // 查询用户id
        Integer userId = userFeignClient.selectUserId();
        if (ObjectUtil.isEmpty(userId)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
        }

        HashMap<String, Object> map = new HashMap<>();

        // 查询用户是否喜欢该文章
        LikesBehavior likesBehavior = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(userId).and("articleId").is(infoDto.getArticleId())), LikesBehavior.class);
        if (ObjectUtil.isNotEmpty(likesBehavior)) {
            map.put("islike", true);
        } else {
            map.put("islike", false);
        }

        // 查询用户是否不喜欢该文章
        UnLikesBehavior unLikesBehavior = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(userId).and("articleId").is(infoDto.getArticleId())), UnLikesBehavior.class);
        if (ObjectUtil.isNotEmpty(unLikesBehavior)) {
            map.put("isunlike", true);
        } else {
            map.put("isunlike", false);
        }

        // 查询用户是否收藏该文章
        CollectionBehavior collectionBehavior = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(userId).and("entryId").is(infoDto.getArticleId())), CollectionBehavior.class);
        if (ObjectUtil.isNotEmpty(collectionBehavior)) {
            map.put("iscollection", true);
        } else {
            map.put("iscollection", false);
        }

        // 查询用户是否关注该作者
        UserRelation userRelation = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(userId).and("authorId").is(infoDto.getAuthorId())), UserRelation.class);
        if (ObjectUtil.isNotEmpty(userRelation)) {
            map.put("isfollow", true);
        } else {
            map.put("isfollow", false);
        }

        return ResponseResult.okResult(map);
    }

    /**
     * 根据频道id查询文章（查询此频道是否有文章）
     *
     * @param id 频道id
     * @return ResponseResult
     */
    @Override
    public ResponseResult findArticleByChannelId(Integer id) {
        // 校验参数的合法性
        if (ObjectUtil.isEmpty(id)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        List<Article> list = list(Wrappers.lambdaQuery(Article.class).eq(Article::getChannelId, id));
        if (CollUtil.isNotEmpty(list)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        return ResponseResult.okResult(true);
    }

    /**
     * 查询最近5天的文章数据（未下架 未删除）
     *
     * @return boolean
     */
    @Override
    public ResponseResult selectFiveDaysArticle() {
        // 查询最近5天未删除 未下架的数据
        // 获取5天前的时间
        // DateTime.now().offset(DateField.DAY_OF_MONTH, -5);

        // 1.获取最近五天的数据 倒序
        List<Article> articleList = list(Wrappers.lambdaQuery(Article.class)
                .gt(Article::getPublishTime, DateTime.now().offset(DateField.DAY_OF_MONTH, -5))
                .orderByDesc(Article::getPublishTime));

        ArrayList<Article> articles = new ArrayList<>();
        for (Article article : articleList) {
            // 查询未删除 未下架的数据
            ArticleConfig articleConfig = configService.getOne(Wrappers.lambdaQuery(ArticleConfig.class)
                    .eq(ArticleConfig::getArticleId, article.getId())
                    .eq(ArticleConfig::getIsDown, 0)
                    .eq(ArticleConfig::getIsDelete, 0));

            // 得到五天内 未删除 未下架的文章
            if (ObjectUtil.isNotEmpty(articleConfig)) {
                articles.add(article);
            }
        }

        // 计算文章的得分
        HashMap<Integer, List<HotArticleVo>> map = new HashMap<>();

        // 符合条件的所有热点文章 __all__
        final ArrayList<HotArticleVo> list = new ArrayList<>();
        for (Article article : articles) {
            // 文章的分数
            Integer articleScore = 0;

            // 阅读 1
            if (ObjectUtil.isNotEmpty(article.getViews())) {
                articleScore += article.getViews();
            }

            // 点赞 3
            if (ObjectUtil.isNotEmpty(article.getViews())) {
                articleScore += article.getLikes() * 3;
            }

            // 评论 5
            if (ObjectUtil.isNotEmpty(article.getViews())) {
                articleScore += article.getComment() * 5;
            }

            // 收藏 8
            if (ObjectUtil.isNotEmpty(article.getViews())) {
                articleScore += article.getCollection() * 8;
            }

            // 得到热门文章
            HotArticleVo hotArticleVo = new HotArticleVo();
            BeanUtil.copyProperties(article, hotArticleVo);
            hotArticleVo.setScore(articleScore);

            // 给对应频道添加热门文章
            List<HotArticleVo> hotArticleVos = map.get(hotArticleVo.getChannelId());
            if (CollUtil.isEmpty(hotArticleVos)) {
                hotArticleVos = new ArrayList<>();
                hotArticleVos.add(hotArticleVo);
                map.put(hotArticleVo.getChannelId(), hotArticleVos);
            } else {
                hotArticleVos.add(hotArticleVo);
            }

            // 排序
            hotArticleVos.sort((o1, o2) -> o2.getScore() - o1.getScore());
        }

        // 每个频道的热门文章不超过30条
        for (Integer channelId : map.keySet()) {
            List<HotArticleVo> hotArticleVoList = map.get(channelId);
            if (hotArticleVoList.size() > 30) {
                hotArticleVoList = hotArticleVoList.subList(0, 30);
            }

            // 每个频道的文章数据添加到 __all__
            list.addAll(hotArticleVoList);

            // 将每个频道的人们文章 保存到Redis中
            redisTemplate.opsForValue().set(ArticleConstants.ARTICLE_CHANNEL + channelId, JSONUtil.toJsonStr(hotArticleVoList), 1, TimeUnit.DAYS);
        }

        // 存未分组数据 __all__
        if (list.size() > 30) {
            redisTemplate.opsForValue().set(ArticleConstants.ARTICLE_CHANNEL + "__all__", JSONUtil.toJsonStr(list.subList(0, 30)), 1, TimeUnit.DAYS);
        } else {
            redisTemplate.opsForValue().set(ArticleConstants.ARTICLE_CHANNEL + "__all__", JSONUtil.toJsonStr(list), 1, TimeUnit.DAYS);
        }

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 接收kafka消息后更新MYSQL和Redis
     * @param streamMess ArticleVisitStreamMess
     */
    @Override
    public void updateScore(ArticleVisitStreamMess streamMess) {
        // 查询当前文章
        Article article = getById(streamMess.getArticleId());
        article.setCollection(streamMess.getCollect());
        article.setLikes(streamMess.getLike());
        article.setViews(streamMess.getView());
        article.setComment(streamMess.getComment());

        // 更新MYSQL
        updateById(article);

        // 获取当前文章要更新的分数
        int collect = streamMess.getCollect();
        int comment = streamMess.getComment();
        int like = streamMess.getLike();
        int view = streamMess.getView();
        int newScore = (view + like * 3 + comment * 5 + collect * 8) * 3;

        // 准备需要更新到Redis中的热门文章数据
        HotArticleVo articleVo = new HotArticleVo();
        BeanUtil.copyProperties(article, articleVo);
        articleVo.setScore(newScore);

        // 得到Redis中相应的热门文章
        String allArticles = redisTemplate.opsForValue().get(ArticleConstants.ARTICLE_CHANNEL + "__all__");
        String channelArticles = redisTemplate.opsForValue().get(ArticleConstants.ARTICLE_CHANNEL + article.getChannelId());

        // 更新热门文章到Redis
        updateHotArticleVoToRedis(allArticles, articleVo, "__all__");
        updateHotArticleVoToRedis(allArticles, articleVo, Convert.toStr(article.getChannelId()));
    }

    /**
     * 更新热门文章到Redis
     * @param articles 热门文章所属Redis
     * @param articleVo 热门文章
     * @param type 文章所属频道
     */
    private void updateHotArticleVoToRedis(String articles, HotArticleVo articleVo, String type) {
        if (StrUtil.isNotEmpty(articles)) {
            // 获取推荐的热门文章数据
            List<HotArticleVo> allHotArticleVos = JSONUtil.toList(articles, HotArticleVo.class);
            // 先判断当前的article是否在缓存中
            boolean isExist = false;

            for (HotArticleVo hotArticleVo : allHotArticleVos) {
                // 当前文章已存在于Redis中
                if (hotArticleVo.getId().equals(articleVo.getId())) {
                    // 替换掉缓存中的article
                    hotArticleVo.setScore(articleVo.getScore());
                    isExist = true;
                    break;
                }
            }

            if (!isExist) {
                // 判断Redis中的数据长度
                if (allHotArticleVos.size() >= 30) {
                    // 替换分数最低的
                    if (allHotArticleVos.get(allHotArticleVos.size() - 1).getScore() < articleVo.getScore()) {
                        allHotArticleVos.remove(allHotArticleVos.size() - 1);
                        allHotArticleVos.add(articleVo);
                    }

                } else {
                    // 不到30条直接添加
                    allHotArticleVos.add(articleVo);
                }
            }

            // 把更新后的数据写回到Redis
            redisTemplate.opsForValue().set(ArticleConstants.ARTICLE_CHANNEL + type, JSONUtil.toJsonStr(allHotArticleVos), 1, TimeUnit.DAYS);
        }
    }
}




